module(..., package.seeall)
local type,tostring,select,pairs=type,tostring,select,pairs
local string_len,string_rep,string_match,string_format,string_find,string_byte=
	  string.len,string.rep,string.match,string.format,string.find,string.byte
local table_insert,table_remove=
	  table.insert,table.remove

require("zdc.sandbox")
local fast_do = zdc.sandbox.fast_do

function newStrStack ()
	return {}
end
function addString(stack, ...)
	for i = 1, select("#",...) do
		local s=tostring(select(i,...))
		if #s>0 then
			table_insert(stack, s)
		end
	end
	for i=#stack-1, 1, -1 do
		if string_len(stack[i]) > string_len(stack[i+1]) then
			break
		end
		stack[i] = stack[i] .. table_remove(stack)
	end
end

local function align(n) return math.ceil(n/16) * 16 end
function hex_dump(buf,first,last)
	local st=''
	for i=(align((first or 1)-16)+1),align(math.min(last or #buf,#buf)) do
	if (i-1) % 16 == 0 then st=st..string_format('%08X  ', i-1) end
		st=st..( i > #buf and '   ' or string_format('%02X ', buf:byte(i)) )
		if i %  8 == 0 then st=st..(' ') end
		if i % 16 == 0 then st=st..( buf:sub(i-16+1, i):gsub('[^ -~]','.')..'\n' ) end
	end
	return st
end

function to_lines_table(s)
	local ts = {}
	if type(s)~='string' then return ts end
	local posa = 1
	local i=1
	while 1 do
		local pos, chars = s:match('()([\r\n].?)', posa)
		if pos then
			table.insert(ts,string.format('%3d:%s',i,s:sub(posa, pos-1)))
			if chars == '\r\n' then posa = pos + 2 else posa = pos + 1 end
			i=i+1
		else 
			table.insert(ts,string.format('%3d:%s',i,s:sub(posa)))
			i=i+1
			break
		end
	end
	return ts
end

function to_code_err(title,code,err,env)
	local inft={
		"to_code_err:",title,
		"Error msg: "..err,
		"Error on: "..string.gsub(err, "^.+%]%:(%d+)%:", "On line %1:"),
	}
	if type(code)=='string' then
		table.insert(inft,"Source code: \n"..table.concat(to_lines_table(code),'\n'))
	end
	if env then
		table.insert(inft,"environment:")
		for k,v in pairs(env) do
			table.insert(inft,string.format("%s=%q", tostring(k), tostring(v)))
		end
	end
	return table.concat(inft,'\n')
end

function i18n(s,l)
	if not s then return end
	local ty=type(s)
	if ty=='string' then
		return s
	elseif ty=='table' then
		return i18n(s[l or 1] or s[1])
	end
	return tostring(s)
end

function get_doc(doc,k,l)
	if doc then
		local s=doc[k]
		return i18n(s,l)
	end
	return nil
end

function string_q(data)
	if string_find(data,"%z") then return string_format("%q", data) end
	-- if the string contains any newlines, find a version of long quotes that will work
	if string_find(data,"\n") then
		local count = 0
		if string_byte(data,#data)==93 then count=1 end
		local rec=string_rep("=", count)
		local open = string_format("[%s[", rec)
		local close = string_format("]%s]", rec)

		while string_find(data,open, nil, true) or string_find(data,close, nil, true) do
			count = count + 1
			rec=string_rep("=", count)
			open = string_format("[%s[", rec)
			close = string_format("]%s]", rec)
		end

		return string_format("%s%s%s", open, data, close)
	else
		return string_format("%q", data)
	end 
end

function sort(a,b)
	local ta,tb=type(a),type(b)
	if ta==tb then return a<b end
	if ta=='number' then return true end
	return tostring(a)<tostring(b)
end

-- 
---Converts an arbitrary data type into a string. Will recursively convert
-- tables.
-- 
--@param data   The data to convert.
--@param indent (optional) The number of times to indent the line. Default
--				is 0.
--@return A string representation of a data, will be one or more full lines.

function to_string_ind(data, indent,sort)
	if(indent == nil) then
		indent = 0
	end
	local indstr=string_rep('\t',indent)

	-- Check the type
	if(type(data) == "string") then
		return indstr .. string_q(data)
	elseif(type(data) == "number") then 
		return indstr .. string_format('%.16g',data)
	elseif(type(data) == "boolean") then 
		if(data == true) then 
			return indstr .. "true"
		else
			return indstr .. "false"
		end
	elseif(type(data) == "table") then
		if not next(data) then return '' end
		local str = newStrStack()
		if sort then
			local st={}
			for i, v in pairs(data) do
				table.insert(st,i)
			end
			sort(st)
			for idx=1,#st do
				local i=st[idx]
				local v=data[i]
				if(type(v) == "table") then 
					addString(str,indstr,tostring(i),":\n",to_string_ind(v, indent + 1,sort),"\n")
				else 
					addString(str,indstr,tostring(i),": ",to_string_ind(v,0,sort),"\n")
				end
			end
		else
			for i, v in pairs(data) do
				-- Check for a table in a table 
				if(type(v) == "table") then 
					addString(str,indstr,tostring(i),":\n",to_string_ind(v, indent + 1),"\n")
				else 
					addString(str,indstr,tostring(i),": ",to_string(v),"\n")
				end
			end
		end
		return table.concat(str)
	else
		return tostring(data)
	end
end


function to_string(data,sort)
	return to_string_ind(data,0,sort)
end

local lua51_keywords =
{
  ["and"] = true,    ["break"] = true,  ["do"] = true,
  ["else"] = true,   ["elseif"] = true, ["end"] = true,
  ["false"] = true,  ["for"] = true,    ["function"] = true,
  ["if"] = true,     ["in"] = true,     ["local"] = true,
  ["nil"] = true,    ["not"] = true,    ["or"] = true,
  ["repeat"] = true, ["return"] = true, ["then"] = true,
  ["true"] = true,    ["until"] = true,  ["while"] = true
}

function serialize(o,i,ikv,sort)
	i=i or 0
	local ide = string_rep('\t',i)
	local t=type(o)
	if t == "string" then
		return ((ikv and '') or ide)..string_format("%q", o)
	elseif t == 'number' then
		return ((ikv and '') or ide)..string_format('%.16g',o)
	elseif t == "boolean" then
		return ((ikv and '') or ide)..((o and 'true') or 'false')
	elseif t == "table" then
		if not next(o) then return ((ikv and '') or ide).."{}" end
		local str = newStrStack()
		addString(str,((ikv and '') or ide),"{\n")
		local notfirst=nil
		if sort then
			local st={}
			for k,v in pairs(o) do
				table.insert(st,k)
			end
			sort(st)
			for idx=1,#st do
				local k=st[idx]
				local v=o[k]
				notfirst=true
				if type(k)== 'number' then
					addString(str,ide ,"[" .. k .. "]=")
				else
					if not lua51_keywords[k] and string_match(k, "^[%a_][%a%d_]*$") then
						addString(str,ide,k,"=")
					else
						addString(str,ide,"[",string_format("%q",k) .. "]=")
					end
				end
				if type(v)=='string' then
					addString(str,string_q(v),';\n')
				else
					addString(str,serialize(v,i+1,true,sort),';\n')
				end
			end
		else
			for k,v in pairs(o) do
				notfirst=true
				if type(k)== 'number' then
					addString(str,ide ,"[" .. k .. "]=")
				else
					if not lua51_keywords[k] and string_match(k, "^[%a_][%a%d_]*$") then
						addString(str,ide,k,"=")
					else
						addString(str,ide,"[",string_format("%q",k) .. "]=")
					end
				end
				if type(v)=='string' then
					addString(str,string_q(v),';\n')
				else
					addString(str,serialize(v,i+1,true),';\n')
				end
			end
		end
		addString(str,string_rep('\t',i-1),"}")
		return table.concat(str)
	else
		error("cannot serialize a " .. t)
	end
	
end

function unserialize(s)
	return fast_do({},'return ' .. s)
end

function str_print(...)
	local str=newStrStack()
	for i = 1, select("#",...) do
		if i>1 then
			addString(str,'\t')
		end
		addString(str,(to_string(select(i,...),sort)))
	end
	addString(str,'\n')
	return table.concat(str)
end


function to_string_dbg(data, indent,sort)
	if(indent == nil) then
		indent = 0
	end
	local indstr=string_rep('\t',indent)

	-- Check the type
	if(type(data) == "string") then
		if #data>1024 and string.find(data,"[%c%z]") then
			return indstr .. '[string:'..#data..']'
		else
			return indstr .. data
		end
	elseif(type(data) == "number") then 
		return indstr .. string_format('%.16g',data)
	elseif(type(data) == "boolean") then 
		if(data == true) then 
			return indstr .. "true"
		else
			return indstr .. "false"
		end
	elseif(type(data) == "table") then
		local str = newStrStack()
		if sort then
			local st={}
			for i, v in pairs(data) do
				table.insert(st,i)
			end
			sort(st)
			for idx=1,#st do
				local i=st[idx]
				local v=data[i]
				if(type(v) == "table") then 
					addString(str,indstr,tostring(i),":\n",to_string_dbg(v, indent + 1,sort),"\n")
				else 
					addString(str,indstr,tostring(i),": ",to_string_dbg(v,0,sort),"\n")
				end
			end
		else
			for i, v in pairs(data) do
				-- Check for a table in a table 
				if(type(v) == "table") then 
					addString(str,indstr,tostring(i),":\n",to_string_dbg(v, indent + 1),"\n")
				else 
					addString(str,indstr,tostring(i),": ",to_string_dbg(v),"\n")
				end
			end
		end
		return table.concat(str)
	else
		return tostring(data)
	end
end
function str_print_dbg(...)
	local str=newStrStack()
	for i = 1, select("#",...) do
		if i>1 then
			addString(str,'\t')
		end
		addString(str,(to_string_dbg(select(i,...),0,sort)))
	end
	addString(str,'\n')
	return table.concat(str)
end

--Adjust the elements' length of a table
--@param data_ta Table to adjust
--@param len_ta  The reference table, has the same key as data_ta
function adjust_len(data_ta,len_ta)
	for k,v in pairs(len_ta) do
		local s = tostring(data_ta[k])
		local l = #s 
		if l < v then
			data_ta[k] = s .. string_rep('\t',v-l)
		end
	end
	return data_ta
end

local _tostring=tostring 
function _M.tostring(value)
  local str = ''

  if (type(value) ~= 'table') then
    if (type(value) == 'string') then
      str = string_format("%q", value)
    elseif (type(value) == 'number') then
    	str = string_format('%.16g',value)
    else
      str = _tostring(value)
    end
  else
    local auxTable = {}
    table.foreach(value, function(i, v)
        table_insert(auxTable, i)
    end)
    sort(auxTable)

    str = str..'{'
    local separator = ""
    local entry = ""
    table.foreachi (auxTable, function (i, fieldName)
      if type(fieldName)=='number' then
        entry = '['..fieldName.."]="..tostring(value[fieldName])
      else
        entry = fieldName.."="..tostring(value[fieldName])
      end
      str = str..separator..entry
      separator = ", "
    end)
    str = str..'}'
  end
  return str
end